#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#




"""Handler library for Blobstore API.

Contains handlers to help with uploading and downloading blobs.

Public Classes:
  BlobstoreDownloadHandler: Has helper method for easily sending blobs
    to client.
  BlobstoreUploadHandler: Handler for receiving upload notification requests.

Public Exceptions (indentation indications class hierarchy):
  Error: Base class for service handler errors.
    RangeFormatError: Raised when Range header has invalid format.
      UnsupportedRangeFormatError: Raised when range header has valid format
        but a particular feature or unit type is not supported.
"""








import cgi
import cStringIO
import os
import re
import sys

from google.appengine.ext import blobstore
from google.appengine.ext import webapp

from webob import byterange


__all__ = [
    'Error',
    'RangeFormatError',
    'UnsupportedRangeFormatError',

    'BlobstoreDownloadHandler',
    'BlobstoreUploadHandler',
]


_CONTENT_DISPOSITION_FORMAT = 'attachment; filename="%s"'

_SEND_BLOB_PARAMETERS = frozenset(['use_range'])

_RANGE_NUMERIC_FORMAT = r'([0-9]*)-([0-9]*)'
_RANGE_FORMAT = r'([a-zA-Z]+)=%s' % _RANGE_NUMERIC_FORMAT
_RANGE_FORMAT_REGEX = re.compile('^%s$' % _RANGE_FORMAT)
_UNSUPPORTED_RANGE_FORMAT_REGEX = re.compile(
    '^%s(?:,%s)+$' % (_RANGE_FORMAT, _RANGE_NUMERIC_FORMAT))
_BYTES_UNIT = 'bytes'


class Error(Exception):
  """Base class for all errors in blobstore handlers module."""

if not hasattr(webapp, 'Error'):
  class RangeFormatError(Error):
    """Raised when Range header incorrectly formatted."""
else:
  class RangeFormatError(webapp.Error):
    """Raised when Range header incorrectly formatted."""


class UnsupportedRangeFormatError(RangeFormatError):
  """Raised when Range format is correct, but not supported."""


def _serialize_range(start, end):
  """Return a string suitable for use as a value in a RANGE header.

  Args:
    start: The start of the bytes range e.g. 50.
    end: The end of the bytes range e.g. 100. This value is inclusive and may
      be None if the end of the range is not specified.

  Returns:
    Returns a string (e.g. "bytes=50-100") that represents a serialized RANGE
    header value.
  """
  if hasattr(byterange.Range, 'serialize_bytes'):
    return byterange.Range.serialize_bytes(_BYTES_UNIT, [(start, end)])
  else:
    if end is not None:
      end += 1
    return str(byterange.Range([(start, end)]))


def _parse_bytes(range_header):
  """Returns a list of byte ranges given the value of a RANGE header."""






  if not hasattr(byterange.Range, 'serialize_bytes'):
    parse_result = byterange.Range.parse_bytes(range_header)
    if parse_result is None:
      return None

    ranges = []
    for start, end in parse_result[1]:
      if end is not None:
        end -= 1
      ranges.append((start, end))
    return parse_result[0], ranges
  else:



    original_stdout = sys.stdout
    sys.stdout = cStringIO.StringIO()
    try:
      return byterange.Range.parse_bytes(range_header)
    finally:
      sys.stdout = original_stdout


def _check_ranges(start, end, use_range_set, use_range, range_header):
  """Set the range header.

  Args:
    start: As passed in from send_blob.
    end: As passed in from send_blob.
    use_range_set: Use range was explcilty set during call to send_blob.
    use_range: As passed in from send blob.
    range_header: Range header as received in HTTP request.

  Returns:
    Range header appropriate for placing in blobstore.BLOB_RANGE_HEADER.

  Raises:
    ValueError if parameters are incorrect.  This happens:
      - start > end.
      - start < 0 and end is also provided.
      - end < 0
      - If index provided AND using the HTTP header, they don't match.
        This is a safeguard.
  """
  if end is not None and start is None:
    raise ValueError('May not specify end value without start.')


  use_indexes = start is not None
  if use_indexes:
    if end is not None:


      if start > end:
        raise ValueError('start must be < end.')

    range_indexes = _serialize_range(start, end)


  if use_range_set and use_range and use_indexes:
    if range_header != range_indexes:
      raise ValueError('May not provide non-equivalent range indexes and '
                       'range headers: (header) %s != (indexes) %s'
                       % (range_header, range_indexes))


  if use_range and range_header is not None:
    return range_header
  elif use_indexes:
    return range_indexes
  else:
    return None


class BlobstoreDownloadHandler(webapp.RequestHandler):
  """Base class for creating handlers that may send blobs to users."""


  __use_range_unset = object()
  def send_blob(self,
                blob_key_or_info,
                content_type=None,
                save_as=None,
                start=None,
                end=None,
                **kwargs):
    """Send a blob-response based on a blob_key.

    Sets the correct response header for serving a blob.  If BlobInfo
    is provided and no content_type specified, will set request content type
    to BlobInfo's content type.

    Args:
      blob_key_or_info: BlobKey or BlobInfo record to serve.
      content_type: Content-type to override when known.
      save_as: If True, and BlobInfo record is provided, use BlobInfos
        filename to save-as.  If string is provided, use string as filename.
        If None or False, do not send as attachment.
      start: Start index of content-range to send.
      end: End index of content-range to send.  End index is inclusive.
      use_range: Use provided content range from requests Range header.
        Mutually exclusive to start and end.

    Raises:
      ValueError on invalid save_as parameter.
    """
    if set(kwargs) - _SEND_BLOB_PARAMETERS:
      invalid_keywords = []
      for keyword in kwargs:
        if keyword not in _SEND_BLOB_PARAMETERS:
          invalid_keywords.append(keyword)
      if len(invalid_keywords) == 1:
        raise TypeError('send_blob got unexpected keyword argument %s.'
                        % invalid_keywords[0])
      else:
        raise TypeError('send_blob got unexpected keyword arguments: %s'
                        % sorted(invalid_keywords))



    use_range = kwargs.get('use_range', self.__use_range_unset)
    use_range_set = use_range is not self.__use_range_unset

    range_header = _check_ranges(start,
                                 end,
                                 use_range_set,
                                 use_range,
                                 self.request.headers.get('range', None))

    if range_header is not None:
      self.response.headers[blobstore.BLOB_RANGE_HEADER] = range_header

    if isinstance(blob_key_or_info, blobstore.BlobInfo):
      blob_key = blob_key_or_info.key()
      blob_info = blob_key_or_info
    else:
      blob_key = blob_key_or_info
      blob_info = None

    self.response.headers[blobstore.BLOB_KEY_HEADER] = str(blob_key)

    if content_type:
      if isinstance(content_type, unicode):
        content_type = content_type.encode('utf-8')
      self.response.headers['Content-Type'] = content_type
    else:


      del self.response.headers['Content-Type']

    def send_attachment(filename):
      if isinstance(filename, unicode):
        filename = filename.encode('utf-8')
      self.response.headers['Content-Disposition'] = (
          _CONTENT_DISPOSITION_FORMAT % filename)

    if save_as:
      if isinstance(save_as, basestring):
        send_attachment(save_as)
      elif blob_info and save_as is True:
        send_attachment(blob_info.filename)
      else:
        if not blob_info:
          raise ValueError('Expected BlobInfo value for blob_key_or_info.')
        else:
          raise ValueError('Unexpected value for save_as.')

    self.response.clear()

  def get_range(self):
    """Get range from header if it exists.

    A range header of "bytes: 0-100" would return (0, 100).

    Returns:
      Tuple (start, end):
        start: Start index.  None if there is None.
        end: End index (inclusive).  None if there is None.
      None if there is no request header.

    Raises:
      UnsupportedRangeFormatError: If the range format in the header is
        valid, but not supported.
      RangeFormatError: If the range format in the header is not valid.
    """
    range_header = self.request.headers.get('range', None)
    if range_header is None:
      return None

    try:
      parsed_range = _parse_bytes(range_header)
    except TypeError, err:
      raise RangeFormatError('Invalid range header: %s' % err)
    if parsed_range is None:
      raise RangeFormatError('Invalid range header: %s' % range_header)

    units, ranges = parsed_range
    if len(ranges) != 1:
      raise UnsupportedRangeFormatError(
          'Unable to support multiple range values in Range header.')

    if units != _BYTES_UNIT:
      raise UnsupportedRangeFormatError(
          'Invalid unit in range header type: %s', range_header)

    return ranges[0]


class BlobstoreUploadHandler(webapp.RequestHandler):
  """Base class for creation blob upload handlers."""

  def __init__(self, *args, **kwargs):
    super(BlobstoreUploadHandler, self).__init__(*args, **kwargs)
    self.__uploads = None

  def get_uploads(self, field_name=None):
    """Get uploads sent to this handler.

    Args:
      field_name: Only select uploads that were sent as a specific field.

    Returns:
      A list of BlobInfo records corresponding to each upload.
      Empty list if there are no blob-info records for field_name.
    """
    if self.__uploads is None:
      self.__uploads = {}
      for key, value in self.request.params.items():
        if isinstance(value, cgi.FieldStorage):
          if 'blob-key' in value.type_options:
            self.__uploads.setdefault(key, []).append(
                blobstore.parse_blob_info(value))

    if field_name:
      try:
        return list(self.__uploads[field_name])
      except KeyError:
        return []
    else:
      results = []
      for uploads in self.__uploads.itervalues():
        results += uploads
      return results
